/**
 * 版权所有(C)，上海勾芒信息科技，2017，所有权利保留。
 * 
 * 项目名：	cards-web
 * 文件名：	WxServiceSupport.java
 * 模块说明：	
 * 修改历史：
 * 2017年5月24日 - Debenson - 创建。
 */
package me.chanjar.weixin.common;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.http.Consts;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import me.chanjar.weixin.common.bean.result.WxError;
import me.chanjar.weixin.common.exception.WxErrorException;
import me.chanjar.weixin.common.util.JsonMapper;
import me.chanjar.weixin.common.util.http.HttpType;
import me.chanjar.weixin.common.util.http.RequestExecutor;
import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor;
import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor;

/**
 * 提供一个微信网络的默认实现类，其它微信服务可继承本类。
 * 
 * @author Debenson
 * @since 0.1
 */
public abstract class WxNetworkingSupport implements WxNetworking {
  final Logger logger = LoggerFactory.getLogger(getClass());

  private int retrySleepMillis = 1000;
  private int maxRetryTimes = 5;

  @Override
  public String httpGet(String uri, Map<String, Object> queryParams) throws WxErrorException {
    StringBuffer sb = new StringBuffer();
    if (queryParams != null) {
      for (Map.Entry<String, Object> entry : queryParams.entrySet()) {
        if (sb.length() != 0) {
          sb.append("&");
        }
        sb.append(entry.getKey()).append("=").append(entry.getValue());
      }
    }
    return httpGet(uri, sb.toString());
  }

  @Override
  public <T> T httpGet(String url, Map<String, Object> queryParams, Class<T> respClazz)
      throws WxErrorException {
    String resp = httpGet(url, queryParams);
    return JsonMapper.defaultMapper().fromJson(resp, respClazz);
  }

  @Override
  public String httpGet(String url, String queryParams) throws WxErrorException {
    return execute(SimpleGetRequestExecutor.create(this), url, queryParams);
  }

  @Override
  public <T> T httpGet(String url, String queryParams, Class<T> respClazz) throws WxErrorException {
    String resp = httpGet(url, queryParams);
    return JsonMapper.defaultMapper().fromJson(resp, respClazz);
  }

  @Override
  public String httpPost(String url, String postData) throws WxErrorException {
    return execute(SimplePostRequestExecutor.create(this), url, postData);
  }

  @Override
  public <T> T httpPost(String url, Object postObj, Class<T> respClazz) throws WxErrorException {
    String resp = httpPost(url, JsonMapper.defaultMapper().toJson(postObj));
    return JsonMapper.defaultMapper().fromJson(resp, respClazz);
  }

  @Override
  public File download(String url) throws WxErrorException {
    try {
      return httpDownload(appendAccessToken(url));
    } catch (WxErrorException e) {
      if (expireIfInvalidToken(e.getError().getErrorCode())) {
        logger.warn("token invalid: {}, will refresh.", e.getError().getErrorCode());
        return httpDownload(appendAccessToken(url));
      }
      throw e;
    }
  }

  @Override
  public InputStream httpPostForStream(String url, String post) throws WxErrorException {
    try {
      return httpCopyFromStream(appendAccessToken(url), post);
    } catch (WxErrorException e) {
      if (expireIfInvalidToken(e.getError().getErrorCode())) {
        logger.warn("token invalid: {}, will refresh.", e.getError().getErrorCode());
        return httpCopyFromStream(appendAccessToken(url), post);
      }
      throw e;
    }
  }

  /**
   * 向微信端发送请求，在这里执行的策略是当发生access_token过期时才去刷新，然后重新执行请求，而不是全局定时请求
   */
  public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data)
      throws WxErrorException {
    return execute(executor, uri, data, true);
  }

  /**
   * @param executor
   * @param uri
   * @param data
   * @param retry
   *          是否重试
   * @return
   * @throws WxErrorException
   */
  protected <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data, boolean retry)
      throws WxErrorException {
    int retryTimes = 0;
    do {
      try {
        T result = executeInternal(executor, uri, data, retry);
        logger.debug("\n[URL]:  {}\n[PARAMS]: {}\n[RESPONSE]: {}", uri, data, result);
        return result;
      } catch (WxErrorException e) {
        if (retryTimes + 1 > this.maxRetryTimes) {
          logger.warn("重试达到最大次数【{}】", maxRetryTimes);
          // 最后一次重试失败后，直接抛出异常，不再等待
          throw new RuntimeException("微信服务端异常，超出重试次数");
        }

        WxError error = e.getError();
        // -1 系统繁忙, 1000ms后重试
        if (error.getErrorCode() == -1) {
          int sleepMillis = this.retrySleepMillis * (1 << retryTimes);
          try {
            logger.warn("微信系统繁忙，{} ms 后重试(第{}次)", sleepMillis, retryTimes + 1);
            Thread.sleep(sleepMillis);
          } catch (InterruptedException e1) {
            throw new RuntimeException(e1);
          }
        } else {
          throw e;
        }
      }
    } while (retryTimes++ < this.maxRetryTimes);

    logger.warn("重试达到最大次数【{}】", this.maxRetryTimes);
    throw new RuntimeException("微信服务端异常，超出重试次数");
  }

  /**
   * @param executor
   * @param uri
   * @param data
   * @param retry
   *          是否重试
   * @return
   * @throws WxErrorException
   */
  protected <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data,
      boolean retry) throws WxErrorException {
    String uriWithAccessToken = appendAccessToken(uri);
    try {
      return executor.execute(uriWithAccessToken, data);
    } catch (WxErrorException e) {
      WxError error = e.getError();
      /*
       * 发生以下情况时尝试刷新access_token 40001
       * 获取access_token时AppSecret错误，或者access_token无效 42001 access_token超时
       */
      if (expireIfInvalidToken(error.getErrorCode())) {
        // 强制设置wxMpConfigStorage它的access token过期了，这样在下一次请求里就会刷新access token
        if (retry) {
          // 不再重试，防止死循环
          return this.execute(executor, uri, data, false);
        }
      }

      if (error.getErrorCode() != 0) {
        logger.error("\n[URL]:  {}\n[PARAMS]: {}\n[RESPONSE]: {}", uri, data, error);
        throw new WxErrorException(error);
      }
      return null;
    } catch (IOException e) {
      logger.error("\n[URL]:  {}\n[PARAMS]: {}\n[EXCEPTION]: {}", uri, data, e.getMessage());
      throw new RuntimeException(e);
    }
  }

  /**
   * 永久素材下载使用,奇葩的下载方式
   * 
   * @param url
   * @param post
   * @return
   * @throws WxErrorException
   */
  protected InputStream httpCopyFromStream(String url, String post) throws WxErrorException {
    HttpPost httpPost = new HttpPost(url);

    if (post != null) {
      StringEntity entity = new StringEntity(post, Consts.UTF_8);
      httpPost.setEntity(entity);
    }

    try (CloseableHttpResponse response = getRequestHttpClient().execute(httpPost)) {
      StatusLine statusLine = response.getStatusLine();
      HttpEntity entity = response.getEntity();
      if (statusLine.getStatusCode() >= 300) {
        EntityUtils.consume(entity);
        throw new WxErrorException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
      }

      InputStream inputStream = entity.getContent();
      byte[] binaryContent = IOUtils.toByteArray(inputStream);
      String content = new String(binaryContent, "UTF-8");
      if (content.contains("errcode")) {
        WxError wxError = WxError.fromJson(content);
        throw new WxErrorException(wxError);
      }

      return new ByteArrayInputStream(binaryContent);
    } catch (IOException e) {
      logger.error("http download: {} failed.", url, e);
      throw new WxErrorException(999, e.getMessage());
    }
  }

  /**
   * @param url
   * @return
   * @throws WxErrorException
   */
  protected File httpDownload(String url) throws WxErrorException {
    HttpGet httpGet = new HttpGet(url);

    try (CloseableHttpResponse response = getRequestHttpClient().execute(httpGet)) {
      StatusLine statusLine = response.getStatusLine();
      HttpEntity entity = response.getEntity();
      if (statusLine.getStatusCode() >= 300) {
        EntityUtils.consume(entity);
        throw new WxErrorException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
      }

      Header[] dispositionHeaders = response.getHeaders("Content-disposition");
      if (dispositionHeaders == null || dispositionHeaders.length <= 0) {
        String errors = entity == null ? null : EntityUtils.toString(entity, Consts.UTF_8);
        logger.warn("download file : {} failed: {}", url, errors);
        if (errors.contains("errcode")) {
          WxError wxError = WxError.fromJson(errors);
          throw new WxErrorException(wxError);
        } else {
          throw new WxErrorException(999, errors);
        }
      }

      String fileName = extractFileName(dispositionHeaders[0].getValue());
      if (fileName == null || "".equals(fileName.trim())) {
        logger.warn("Cannot get filename from Content-disposition");
        fileName = UUID.randomUUID().toString();
      }
      InputStream inputStream = entity.getContent();
      File tempFile = new File(FileUtils.getTempDirectory(), fileName);
      FileUtils.copyInputStreamToFile(inputStream, tempFile);
      return tempFile;
    } catch (IOException e) {
      logger.error("http download: {} failed.", url, e);
      throw new WxErrorException(999, e.getMessage());
    }
  }

  private String extractFileName(String headerValue) {
    String fileName = null;
    Pattern regex = Pattern.compile("(?<=filename=\").*?(?=\")");
    Matcher regexMatcher = regex.matcher(headerValue);
    if (regexMatcher.find()) {
      fileName = regexMatcher.group();
    }
    return fileName;
  }

  public int getRetrySleepMillis() {
    return retrySleepMillis;
  }

  public void setRetrySleepMillis(int retrySleepMillis) {
    this.retrySleepMillis = retrySleepMillis;
  }

  public int getMaxRetryTimes() {
    return maxRetryTimes;
  }

  public void setMaxRetryTimes(int maxRetryTimes) {
    this.maxRetryTimes = maxRetryTimes;
  }

  @Override
  public HttpType getRequestType() {
    return HttpType.APACHE_HTTP;
  }

}
